Skip to content

feat: Gallery/Timeline Spec Config 标准化接入 — nested config, strong types, full test coverage#780

Merged
hotlong merged 4 commits intomainfrom
copilot/standardize-gallery-timeline-config
Feb 23, 2026
Merged

feat: Gallery/Timeline Spec Config 标准化接入 — nested config, strong types, full test coverage#780
hotlong merged 4 commits intomainfrom
copilot/standardize-gallery-timeline-config

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Feb 23, 2026

Standardizes Gallery and Timeline spec config integration across ObjectUI, replacing legacy flat prop spreading with nested config objects and strong types from @objectstack/spec/ui.

Bug fix

In ListView.tsx, both gallery and timeline cases now pass config as nested objects instead of spreading flat:

// Before (broken): cardSize/coverFit/visibleFields lost for gallery, timeline spread flat
return {
  type: 'object-gallery',
  ...baseProps,
  ...(schema.gallery || {}),  // spreads flat — ObjectGallery never sees schema.gallery
};

// After: both gallery and timeline pass nested config objects
return {
  type: 'object-gallery',
  ...baseProps,
  gallery: { ...(schema.options?.gallery || {}), ...(schema.gallery || {}) },
  imageField: schema.gallery?.coverField || ...,  // deprecated top-level fallback kept
};

Strong types

  • ListViewSchema.gallery and .timeline replaced from inline object types → ListViewGalleryConfig / ListViewTimelineConfig (intersection of spec canonical GalleryConfig/TimelineConfig + legacy fields + index signature for backward compat)
  • New types exported from @object-ui/types
  • Spec bridge list-view.ts updated from any → strong ListViewGalleryConfig/ListViewTimelineConfig types

ObjectTimeline nested config support

  • ObjectTimeline now accepts nested schema.timeline.* config (mirrors the ObjectGallery pattern)
  • Fallback chain: timeline.startDateFieldmapping.datestartDateFielddateField'date'
  • Maps timeline.scale → renderer timeScale property for gantt variant rendering
  • Deprecated flat props retained for backward compatibility

ObjectGallery

  • Already reads from schema.gallery.* with legacy imageField/titleField fallbacks — no code changes needed

Tests

  • 12 ObjectGallery tests: coverField, cardSize grid class, coverFit object-contain, visibleFields, titleField, legacy fallbacks, spec-over-legacy priority
  • 13 ObjectTimeline spec config tests: startDateField, endDateField, groupByField, colorField, scale, all scale enum values, backward compat fallbacks, spec-over-flat priority
  • 10 type-level tests validating ListViewSchema accepts spec config, legacy fields, options.* fallback, cardSize/coverFit enums, and gallery+timeline co-existence
  • 70 bridge tests pass (existing, validating gallery/timeline passthrough)
Original prompt

This section details on the original issue you should resolve

<issue_title>[P0] Gallery/Timeline Spec Config 标准化接入</issue_title>
<issue_description>## 背景
Parent Issue: #736

Spec 定义了 GalleryConfigSchemaTimelineConfigSchema,但 ObjectUI 的 Gallery/Timeline 视图仍使用 legacy options.gallery / options.timeline 格式。

目标

  • Gallery 支持 Spec gallery config:coverField, coverFit, cardSize, titleField, visibleFields
  • Timeline 支持 Spec timeline config:startDateField, endDateField, titleField, groupByField, colorField, scale
  • 保持向后兼容:legacy options.* 仍可用作 fallback

技术要点

  • packages/types/src/objectql.ts — ListViewSchema 已有 gallery?: anytimeline?: any,需改为强类型
  • packages/plugin-list/src/ListView.tsx — 视图组件 schema 构建需优先读取 spec config,fallback 到 options
  • packages/react/src/spec-bridge/bridges/list-view.ts — bridge 已传递,需映射到实际 props

改动范围

  • packages/types/src/objectql.ts — 定义 GalleryConfig / TimelineConfig 强类型 interface
  • packages/plugin-list/src/ListView.tsx — Gallery/Timeline schema 构建优先使用 spec config
  • packages/plugin-gallery/src/ObjectGallery.tsx — 接受新的 prop 命名
  • packages/plugin-timeline/src/ObjectTimeline.tsx — 接受新的 prop 命名

验收标准

  • schema.gallery.coverField 驱动 Gallery 封面图
  • schema.gallery.cardSize 控制卡片尺寸(small/medium/large)
  • schema.timeline.scale 控制时间轴刻度(day/week/month/quarter/year)
  • Legacy options.gallery / options.timeline 仍向后兼容
  • 所有新增代码有对应单元测试
  • pnpm test 全部通过
    </issue_description>

Comments on the Issue (you are @copilot in this section)


💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

@vercel
Copy link
Copy Markdown

vercel bot commented Feb 23, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
objectui Ready Ready Preview, Comment Feb 23, 2026 10:49am
objectui-demo Ready Ready Preview, Comment Feb 23, 2026 10:49am
objectui-storybook Error Error Feb 23, 2026 10:49am

Request Review

- Replace inline gallery/timeline types in ListViewSchema with GalleryConfig/TimelineConfig references
- Add ListViewGalleryConfig and ListViewTimelineConfig intersection types with legacy field support
- Fix ListView to pass gallery as nested object so ObjectGallery reads spec props correctly
- Add comprehensive spec config tests for gallery (coverField, cardSize, coverFit, visibleFields)
- Add spec config type tests for timeline (startDateField, scale, groupByField, etc.)
- Add backward compatibility tests for legacy options.gallery/timeline

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Copilot AI changed the title [WIP] Standardize Gallery and Timeline Spec Config integration fix: Pass gallery config as nested object to ObjectGallery; add strong types for Gallery/Timeline config Feb 23, 2026
Copilot AI requested a review from hotlong February 23, 2026 09:48
@hotlong
Copy link
Copy Markdown
Contributor

hotlong commented Feb 23, 2026

@copilot 仔细检查,需要完成任务中所有的开发

Copilot AI and others added 2 commits February 23, 2026 09:59
…timeline as nested object

- ObjectTimeline now supports `schema.timeline.*` nested config with fallback to flat props
- ObjectTimeline maps `timeline.scale` to `timeScale` for the renderer
- ListView timeline case mirrors gallery pattern: passes nested `timeline` config
- Bridge uses strong ListViewGalleryConfig/ListViewTimelineConfig types
- Added 13 ObjectTimeline spec config tests (startDateField, endDateField, groupByField, colorField, scale, backward compat, spec priority)
- Enhanced GalleryTimelineSpecConfig tests with cardSize/coverFit/co-existence coverage

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
… any casts

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Feb 23, 2026

@copilot 仔细检查,需要完成任务中所有的开发

Completed all remaining development tasks in commit 4841df6:

  1. ObjectTimeline — Now accepts nested schema.timeline.* config (mirrors ObjectGallery pattern), with fallback to flat props for backward compat. Maps timeline.scale → renderer timeScale.
  2. ListView timeline case — Updated to pass nested timeline config object (matching gallery pattern), removed old flat spread.
  3. Bridge — Strong typed ListViewGalleryConfig/ListViewTimelineConfig (replaced any).
  4. 13 new ObjectTimeline spec config tests — Covers startDateField, endDateField, groupByField, colorField, scale, backward compat fallbacks, and spec-over-flat priority.
  5. Enhanced type-level tests — Added cardSize/coverFit enum coverage and gallery+timeline co-existence test.

All 388 tests pass (318 plugin + 70 bridge).

Copilot AI changed the title fix: Pass gallery config as nested object to ObjectGallery; add strong types for Gallery/Timeline config feat: Gallery/Timeline Spec Config 标准化接入 — nested config, strong types, full test coverage Feb 23, 2026
@hotlong hotlong marked this pull request as ready for review February 23, 2026 10:31
Copilot AI review requested due to automatic review settings February 23, 2026 10:31
@hotlong hotlong merged commit 34fe884 into main Feb 23, 2026
3 of 5 checks passed
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR standardizes Gallery and Timeline spec config integration across ObjectUI by replacing legacy flat prop spreading with nested config objects and strong types from @objectstack/spec/ui. It fixes a bug where ListView.tsx was spreading config flat instead of passing nested objects, causing spec properties like cardSize, coverFit, and visibleFields to be lost for Gallery, and timeline config to not propagate correctly to ObjectTimeline.

Changes:

  • Introduced ListViewGalleryConfig and ListViewTimelineConfig intersection types combining spec-canonical types with legacy fields for backward compatibility
  • Refactored ListView.tsx to pass gallery and timeline configs as nested objects instead of flat spreads
  • Updated ObjectTimeline to accept and prioritize nested schema.timeline.* config with proper fallback chains
  • Added comprehensive test coverage (35 new tests across gallery, timeline, and type validation)

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
packages/types/src/objectql.ts Defines ListViewGalleryConfig and ListViewTimelineConfig intersection types extending spec types with legacy fields and index signatures
packages/types/src/index.ts Exports the new ListViewGalleryConfig and ListViewTimelineConfig types
packages/react/src/spec-bridge/bridges/list-view.ts Updates spec bridge from any to strong ListViewGalleryConfig/ListViewTimelineConfig types
packages/plugin-list/src/ListView.tsx Refactors gallery and timeline cases to merge spec config over legacy options and pass as nested objects with deprecated flat prop fallbacks
packages/plugin-list/src/__tests__/ObjectGallery.test.tsx Adds 12 spec config integration tests covering coverField, cardSize, coverFit, visibleFields, titleField, and legacy fallbacks
packages/plugin-list/src/__tests__/GalleryTimelineSpecConfig.test.tsx Adds 10 type-level tests validating ListViewSchema accepts spec config, legacy fields, and co-existence scenarios
packages/plugin-timeline/src/ObjectTimeline.tsx Implements nested timeline config support with fallback chain: timeline.*mapping.* → flat props → defaults
packages/plugin-timeline/src/__tests__/ObjectTimeline.spec-config.test.tsx Adds 13 spec config tests for startDateField, endDateField, groupByField, colorField, scale, and backward compatibility

Comment on lines +85 to +95
/**
* Gallery configuration extended with legacy fields for backward compatibility.
* Spec fields from GalleryConfigSchema take priority; legacy fields serve as fallbacks.
*/
export type ListViewGalleryConfig = GalleryConfig & {
/** Legacy: image field (deprecated, use coverField) */
imageField?: string;
/** Legacy: subtitle field */
subtitleField?: string;
[key: string]: any;
};
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to Rule #-1 (English-Only Codebase), all user-facing text in comments, documentation, and code MUST be written in English for international accessibility. The PR title and issue description contain Chinese text ("标准化接入"), which should be translated to English. While this is a critical codebase convention, the actual code implementation here is correct and properly documented in English.

Copilot generated this review using guidance from repository custom instructions.
Comment on lines +774 to +811
case 'gallery': {
// Merge spec config over legacy options into nested gallery prop
const mergedGallery = {
...(schema.options?.gallery || {}),
...(schema.gallery || {}),
};
return {
type: 'object-gallery',
...baseProps,
// Nested gallery config (spec-compliant, used by ObjectGallery)
gallery: Object.keys(mergedGallery).length > 0 ? mergedGallery : undefined,
// Deprecated top-level props for backward compat
imageField: schema.gallery?.coverField || schema.gallery?.imageField || schema.options?.gallery?.imageField,
titleField: schema.gallery?.titleField || schema.options?.gallery?.titleField || 'name',
subtitleField: schema.gallery?.subtitleField || schema.options?.gallery?.subtitleField,
...(schema.gallery?.coverFit ? { coverFit: schema.gallery.coverFit } : {}),
...(schema.gallery?.cardSize ? { cardSize: schema.gallery.cardSize } : {}),
...(schema.gallery?.visibleFields ? { visibleFields: schema.gallery.visibleFields } : {}),
...(groupingConfig ? { grouping: groupingConfig } : {}),
...(schema.options?.gallery || {}),
...(schema.gallery || {}),
};
case 'timeline':
}
case 'timeline': {
// Merge spec config over legacy options into nested timeline prop
const mergedTimeline = {
...(schema.options?.timeline || {}),
...(schema.timeline || {}),
};
return {
type: 'object-timeline',
...baseProps,
// Nested timeline config (spec-compliant, used by ObjectTimeline)
timeline: Object.keys(mergedTimeline).length > 0 ? mergedTimeline : undefined,
// Deprecated top-level props for backward compat
startDateField: schema.timeline?.startDateField || schema.options?.timeline?.startDateField || schema.options?.timeline?.dateField || 'created_at',
titleField: schema.timeline?.titleField || schema.options?.timeline?.titleField || 'name',
...(schema.timeline?.endDateField ? { endDateField: schema.timeline.endDateField } : {}),
...(schema.timeline?.groupByField ? { groupByField: schema.timeline.groupByField } : {}),
...(schema.timeline?.colorField ? { colorField: schema.timeline.colorField } : {}),
...(schema.timeline?.scale ? { scale: schema.timeline.scale } : {}),
...(schema.options?.timeline || {}),
...(schema.timeline || {}),
};
}
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to Rule #2 (Documentation Driven Development), any feature implemented or refactored MUST update corresponding documentation. This PR standardizes Gallery/Timeline config integration but does not update:

  1. packages/plugin-list/README.md - Should document the new spec-compliant gallery and timeline fields in the schema examples and add migration guidance from options.gallery/options.timeline to the new nested config pattern.
  2. packages/plugin-timeline/README.md - Should document the new timeline.* nested config support with field examples (startDateField, endDateField, scale, etc.).
  3. content/docs/plugins/plugin-timeline.mdx - Should show examples of using ObjectTimeline with the spec-compliant nested config.

The definition of done requires that documentation reflect the new code/architecture. Users reading the documentation will not discover the new nested config pattern without these updates.

Copilot generated this review using guidance from repository custom instructions.
Comment on lines +1 to +232
/**
* ObjectUI
* Copyright (c) 2024-present ObjectStack Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import { ObjectTimeline } from '../ObjectTimeline';

// Mock dependencies
vi.mock('@object-ui/react', () => ({
useDataScope: () => undefined,
useNavigationOverlay: () => ({
isOverlay: false,
handleClick: vi.fn(),
selectedRecord: null,
isOpen: false,
close: vi.fn(),
setIsOpen: vi.fn(),
mode: 'page' as const,
width: undefined,
view: undefined,
open: vi.fn(),
}),
}));

vi.mock('@object-ui/components', () => ({
NavigationOverlay: () => null,
}));

vi.mock('@object-ui/mobile', () => ({
usePullToRefresh: () => ({
ref: { current: null },
isRefreshing: false,
pullDistance: 0,
}),
}));

// Mock the TimelineRenderer to inspect the schema it receives
const mockTimelineRenderer = vi.fn(() => <div data-testid="timeline-renderer" />);
vi.mock('../renderer', () => ({
TimelineRenderer: (props: any) => mockTimelineRenderer(props),
}));

describe('ObjectTimeline Spec Config', () => {
beforeEach(() => {
mockTimelineRenderer.mockClear();
});

const mockData = [
{ id: '1', name: 'Event A', start_date: '2024-01-01', end_date: '2024-01-15', category: 'work', priority_color: 'red' },
{ id: '2', name: 'Event B', start_date: '2024-02-01', end_date: '2024-02-28', category: 'personal', priority_color: 'blue' },
];

describe('nested timeline config', () => {
it('uses timeline.startDateField to resolve start dates', () => {
const schema: any = {
type: 'timeline' as const,
objectName: 'events',
timeline: { startDateField: 'start_date', titleField: 'name' },
};
render(<ObjectTimeline schema={schema} data={mockData} />);
expect(mockTimelineRenderer).toHaveBeenCalled();
const renderedSchema = mockTimelineRenderer.mock.calls[0][0].schema;
expect(renderedSchema.items[0].time).toBe('2024-01-01');
expect(renderedSchema.items[0].startDate).toBe('2024-01-01');
});

it('uses timeline.titleField to resolve titles', () => {
const schema: any = {
type: 'timeline' as const,
objectName: 'events',
timeline: { startDateField: 'start_date', titleField: 'name' },
};
render(<ObjectTimeline schema={schema} data={mockData} />);
const renderedSchema = mockTimelineRenderer.mock.calls[0][0].schema;
expect(renderedSchema.items[0].title).toBe('Event A');
expect(renderedSchema.items[1].title).toBe('Event B');
});

it('uses timeline.endDateField to resolve end dates', () => {
const schema: any = {
type: 'timeline' as const,
objectName: 'events',
timeline: { startDateField: 'start_date', endDateField: 'end_date', titleField: 'name' },
};
render(<ObjectTimeline schema={schema} data={mockData} />);
const renderedSchema = mockTimelineRenderer.mock.calls[0][0].schema;
expect(renderedSchema.items[0].endDate).toBe('2024-01-15');
});

it('uses timeline.groupByField to add group property', () => {
const schema: any = {
type: 'timeline' as const,
objectName: 'events',
timeline: { startDateField: 'start_date', titleField: 'name', groupByField: 'category' },
};
render(<ObjectTimeline schema={schema} data={mockData} />);
const renderedSchema = mockTimelineRenderer.mock.calls[0][0].schema;
expect(renderedSchema.items[0].group).toBe('work');
expect(renderedSchema.items[1].group).toBe('personal');
});

it('uses timeline.colorField to add color property', () => {
const schema: any = {
type: 'timeline' as const,
objectName: 'events',
timeline: { startDateField: 'start_date', titleField: 'name', colorField: 'priority_color' },
};
render(<ObjectTimeline schema={schema} data={mockData} />);
const renderedSchema = mockTimelineRenderer.mock.calls[0][0].schema;
expect(renderedSchema.items[0].color).toBe('red');
expect(renderedSchema.items[1].color).toBe('blue');
});

it('uses timeline.scale to set timeScale on rendered schema', () => {
const schema: any = {
type: 'timeline' as const,
objectName: 'events',
timeline: { startDateField: 'start_date', titleField: 'name', scale: 'month' },
};
render(<ObjectTimeline schema={schema} data={mockData} />);
const renderedSchema = mockTimelineRenderer.mock.calls[0][0].schema;
expect(renderedSchema.timeScale).toBe('month');
});

it('timeline.scale supports all valid values', () => {
const scales = ['hour', 'day', 'week', 'month', 'quarter', 'year'] as const;
scales.forEach((scale) => {
mockTimelineRenderer.mockClear();
const schema: any = {
type: 'timeline' as const,
objectName: 'events',
timeline: { startDateField: 'start_date', titleField: 'name', scale },
};
render(<ObjectTimeline schema={schema} data={mockData} />);
const renderedSchema = mockTimelineRenderer.mock.calls[0][0].schema;
expect(renderedSchema.timeScale).toBe(scale);
});
});
});

describe('backward compatibility', () => {
it('falls back to flat startDateField when timeline config is not set', () => {
const schema: any = {
type: 'timeline' as const,
objectName: 'events',
startDateField: 'start_date',
titleField: 'name',
};
render(<ObjectTimeline schema={schema} data={mockData} />);
const renderedSchema = mockTimelineRenderer.mock.calls[0][0].schema;
expect(renderedSchema.items[0].time).toBe('2024-01-01');
});

it('falls back to legacy dateField when startDateField is not set', () => {
const data = [{ id: '1', name: 'Event', legacy_date: '2024-03-15' }];
const schema: any = {
type: 'timeline' as const,
objectName: 'events',
dateField: 'legacy_date',
titleField: 'name',
};
render(<ObjectTimeline schema={schema} data={data} />);
const renderedSchema = mockTimelineRenderer.mock.calls[0][0].schema;
expect(renderedSchema.items[0].time).toBe('2024-03-15');
});

it('falls back to flat groupByField when timeline.groupByField is not set', () => {
const schema: any = {
type: 'timeline' as const,
objectName: 'events',
startDateField: 'start_date',
titleField: 'name',
groupByField: 'category',
};
render(<ObjectTimeline schema={schema} data={mockData} />);
const renderedSchema = mockTimelineRenderer.mock.calls[0][0].schema;
expect(renderedSchema.items[0].group).toBe('work');
});

it('falls back to flat colorField when timeline.colorField is not set', () => {
const schema: any = {
type: 'timeline' as const,
objectName: 'events',
startDateField: 'start_date',
titleField: 'name',
colorField: 'priority_color',
};
render(<ObjectTimeline schema={schema} data={mockData} />);
const renderedSchema = mockTimelineRenderer.mock.calls[0][0].schema;
expect(renderedSchema.items[0].color).toBe('red');
});

it('falls back to flat scale when timeline.scale is not set', () => {
const schema: any = {
type: 'timeline' as const,
objectName: 'events',
startDateField: 'start_date',
titleField: 'name',
scale: 'week',
};
render(<ObjectTimeline schema={schema} data={mockData} />);
const renderedSchema = mockTimelineRenderer.mock.calls[0][0].schema;
expect(renderedSchema.timeScale).toBe('week');
});

it('spec timeline config takes priority over flat props', () => {
const schema: any = {
type: 'timeline' as const,
objectName: 'events',
startDateField: 'end_date', // flat — should be overridden
titleField: 'id', // flat — should be overridden
groupByField: 'priority_color', // flat — should be overridden
timeline: {
startDateField: 'start_date', // spec — takes priority
titleField: 'name', // spec — takes priority
groupByField: 'category', // spec — takes priority
},
};
render(<ObjectTimeline schema={schema} data={mockData} />);
const renderedSchema = mockTimelineRenderer.mock.calls[0][0].schema;
expect(renderedSchema.items[0].time).toBe('2024-01-01');
expect(renderedSchema.items[0].title).toBe('Event A');
expect(renderedSchema.items[0].group).toBe('work');
});
});
});
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to Guideline 1000001, after completing a development task, tests should be run and any issues addressed. The PR description mentions comprehensive test coverage (12 ObjectGallery tests, 13 ObjectTimeline tests, 10 type-level tests, 70 bridge tests), but does not provide evidence of running pnpm test to verify all tests pass. Please run pnpm test (or pnpm vitest run packages/plugin-list packages/plugin-timeline packages/types packages/react) and confirm all tests pass before merging.

Copilot generated this review using guidance from organization custom instructions.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[P0] Gallery/Timeline Spec Config 标准化接入

3 participants